/*-----------------------------------------------------------------------------
	The contents of this file are subject to the Mozilla Public License
	Version 1.1 (the "License"); you may not use this file except in
	compliance with the License.You may obtain a copy of the License at
	http://www.mozilla.org/MPL/MPL-1.1.html
	
	Software distributed under the License is distributed on an "AS IS"
	basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
	License for the specific language governing rights and limitations
	under the License.
	
	The Original Code is Fever Framework code.
	
	The Initial Developer of the Original Code is Romain Ecarnot.
	Portions created by Initial Developer are Copyright (C) 2006
	the Initial Developer. All Rights Reserved.
	
	Contributor(s):
	
	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
-----------------------------------------------------------------------------*/

import com.bourre.commands.Command;
import com.bourre.data.collections.IndexedArray;
import com.bourre.data.collections.Map;
import com.bourre.events.EventBroadcaster;
import com.bourre.events.FrontController;

import fever.app.accelerator.Keyboard;
import fever.app.accelerator.KeyCombo;
import fever.app.accelerator.KeyDefinition;
import fever.events.KeyboardEvent;
import fever.log.FeverDebug;
import fever.utils.Stringifier;

/**
 * Implements key listening system.
 * 
 * <p>2 ways are available to listen to keys event.
 * <ul>
 *   <li>Classic Events implementation.</li>
 *   <li>FrontController implementation.</li>
 * </ul>
 * 
 * <p>If using <strong>Event implementation</strong>, use 
 * {@link #addKeyListener} and {@link #removeKeyListener} methods 
 * to manage key listeners.
 * 
 * <p>If using <strong>FrontController</strong> implementation, 
 * use {@link #push} and {@link #remove} methods to manage key 
 * {@code Command} listener.
 * 
 * <p>Take a look at {@link Keyboard} class to see all 
 * Key constantdefinition.
 * Defines keys sequence using {@link KeyCombo} class.
 *  
 * <p><u>Note :</u> {@code Combo} are not available with {@code KeyUp} listening system.
 * 
 * <p>Example
 * {@code
 *	 var controller : KeyboardController = KeyboardController.getInstance();
 *	
 *	 //defines shortcut with FrontController behaviour
 *	 controller.push( Keyboard.onKeyA, new KeyACommand() );
 *	
 *	 //defines shortcut using event listening
 *	 controller.addKeyListener(Keyboard.onKeyV, this, _onKeyV);
 *	
 *	 //defines a combo
 *	 //Note : Name (Ctrl+T) must be logic, it's used for accessibility shortcut definiton
 *	 controller.push( new KeyCombo( Keyboard.onKeyCONTROL, Keyboard.onKeyT ), new KeyComboCommand() );
 *	 
 *	 //simulate shortcut activation
 *	 controller.broadcastEvent( Keyboard.onKeyG );
 * }
 * 
 * @see fever.app.accelerator.Keyboard
 * @see fever.app.accelerator.KeyCombo
 * 
 * @author Romain Ecarnot
 */
class fever.app.accelerator.KeyboardController extends FrontController
{
	//-------------------------------------------------------------------------
	// Public Properties
	//-------------------------------------------------------------------------
	
	/** Defines if we listen to KeyDown event or not. */
	public static var keyDown : Boolean = true;
	
	/**
	 * Defines if we listen to KeyUp event or not.
	 * 
	 * <p>Note that {@code Combo} are not available with KeyUp system.
	 */
	public static var keyUp : Boolean = false;
	
	
	//-------------------------------------------------------------------------
	// Private properties
	//-------------------------------------------------------------------------
	
	private static var _instance : KeyboardController;
	
	private var _broadcaster : EventBroadcaster;
	private var _comboMap : Map;
	private var _keyDownList : IndexedArray;
	private var _isPending : Boolean;
	private var _isPendingCombo : Boolean;
	private var _pendingComboInterval : Number;
	private var _pendingInterval : Number;
	
	
	//-------------------------------------------------------------------------
	// Public API
	//-------------------------------------------------------------------------
	
	/**
	 * Creates and returns {@code  KeyboardController} instance.
	 * 
	 * <p>Always return same instance.
	 * 
	 * @return {@link KeyboardController} instance
	 */ 
	public static function getInstance() : KeyboardController
	{
		if( _instance == undefined) _instance = new KeyboardController();		
		return _instance;
	}
	
	/**
	 * Force keydown list cleaning.
	 * 
	 * <p>Really useful, for example, when a context dialog box open.
	 * Either Flash player doesn't know about 'KeyUp' state. 
	 */
	public static function clean() : Void
	{
		getInstance()._keyDownList = new IndexedArray();	
	}
	
	/**
	 * Broadcasts specific key event.
	 * 
	 * @param keyEvent {@link Keyboard} key structure name 
	 */
	public function fire( keyEvent : Keyboard ) : Void
	{
		_fireEvent( Keyboard(keyEvent) );
	}
	
	/**
	 * Adds new command definition.
	 * 
	 * @param k {@link Keyboard} or {@link KeyCombo} instance
	 * @param c {@link Command} instance
	 */
	public function push( k : KeyDefinition, c : Command) : Void
	{
		if( k instanceof KeyCombo ) _setCommandForCombo( KeyCombo( k ), c );
		else if ( k instanceof Keyboard ) _setCommandForKey( Keyboard( k ), c );
	}
	
	/**
	 * Removes command definition.
	 * 
	 * @param k {@link Keyboard} or {@link KeyCombo} instance
	 */
	public function remove( k : KeyDefinition ) : Void
	{
		if( k instanceof KeyCombo ) _removeCommandForCombo( KeyCombo( k ) );
		else if ( k instanceof Keyboard ) _removeCommandForKey( Keyboard( k ) );
	}
	
	/**
	 * Adds passed-in {@code listener} for receiving passed-in 
	 * {@code event} type.
	 * 
	 * @param type {@link Keyboard} or {@link KeyCombo} instance
	 * @param listener Listener object.
	 */
	public function addKeyListener( type : KeyDefinition, listener ) : Void
	{
		if( type instanceof KeyCombo )
		{
			var c : KeyCombo = KeyCombo( type );
			arguments[0] = c.key;
			_addCombo( c.combo, c.key );
		}
		_broadcaster.addEventListener.apply( _broadcaster, arguments );
	}
	
	/**
	 * Removes passed-in {@code listener} that suscribed for passed-in {@code type} event.
	 *  
	 * @param type {@link Keyboard} or {@link KeyCombo} instance
	 * @param listener Listener object.
	 */
	public function removeKeyListener( type : KeyDefinition, listener ) : Void
	{
		if( type instanceof KeyCombo )
		{
			_broadcaster.removeEventListener( KeyCombo( type ).key, listener);
			_removeCombo( KeyCombo( type ).combo );
		}
		else
		{
			_broadcaster.removeEventListener( Keyboard( type ), listener);
		}
	}
	
	/**
	 * {@link KeyboardController} instance stringifier.
	 * 
	 * @return {@link KeyboardController} string representation.
	 */
	public function toString() : String
	{
		return Stringifier.parse( this );	
	}
	
	
	//-------------------------------------------------------------------------
	// Private implementation
	//-------------------------------------------------------------------------

	/**
	 * Constructs a new {@code KeyboardController} instance.
	 */
	private function KeyboardController()
	{
		super( new EventBroadcaster(this) );
		
		_broadcaster = new EventBroadcaster(this);
		_keyDownList = new IndexedArray();
		_comboMap = new Map();
		_isPending = false;
		_isPendingCombo = false;
		
		Key.addListener(this);
	}
	
	private function _addCombo(name : String, key : Keyboard) : Void
	{
		if(_comboMap.containsKey(name))
		{
			FeverDebug.WARN( 'Combo ' + name + ' already exist, overrides' );
		} 
		_comboMap.put(name, key);
	}
	
	private function _removeCombo( name : String ) : Void
	{
		_comboMap.remove( name );	
	}
	
	private function _setCommandForKey( k : Keyboard, c : Command ) : Void
	{
		super.push( k, c );	
	}
	 
	private function _setCommandForCombo( k : KeyCombo, c : Command ) : Void
	{
		_addCombo( k.combo, k.key );
		super.push( k.key, c );
	}
	
	private function _removeCommandForKey( k : Keyboard ) : Void
	{
		super.remove( k );	
	}
	 
	private function _removeCommandForCombo( k : KeyCombo ) : Void
	{
		super.remove( k.key  );
		_removeCombo( k.combo );
	}
	
	private function onKeyUp() : Void
	{
		var code : Number = Key.getCode();
		
		_keyDownList.remove( Keyboard.getStruct(code) );
		
		if( keyDown )
		{
			_stopPending();
			if(_isPendingCombo) _stopPendingCombo();
		}
		if( keyUp ) _prepareEvent( code );
	}
	
	private function onKeyDown() : Void
	{
		if( keyDown )
		{
			var code : Number = Key.getCode();
			var k : Keyboard = Keyboard.getStruct(code);
			_keyDownList.push(k);
			
			var key : String = _keyDownList.join( KeyCombo.LIMITER );
			
			if( _comboMap.containsKey( key ) )
			{
				if( !_isPendingCombo )
				{
					var myKey : Keyboard = _comboMap.get(key);
					
					_fireEvent( myKey, null);
					_isPendingCombo = true;
					clearInterval(_pendingComboInterval);
					_pendingComboInterval = setInterval(this, "_stopPendingCombo", myKey.delay);
				}	
			}
			else
			{
				if(!_isPending)
				{
					_isPending = true;
					_fireEvent( k, code);
					clearInterval(_pendingInterval);
					_pendingInterval = setInterval(this, "_stopPending", k.delay);
				}
			}
		}
		
	}
	
	/**
	 * Retreives {@link KeyStruct} instance from passed-in {@code code} 
	 * and broadcast event calling {@link #_fireEvent} method.
	 */
	private function _prepareEvent(code : Number) : Void
	{ 
		_fireEvent( Keyboard.getStruct( code ), code );
	}
	
	/**
	 * Broadcasts event and/or execute registred {@code Command}
	 */
	private function _fireEvent(key : Keyboard, code : Number) : Void {
		var keyCode : Number = ( (code) ? code : Keyboard.getCodeFromStruct(key) );
		
		var event : KeyboardEvent = new KeyboardEvent(key, this);
		event.keyCode = keyCode;
		
		_oEB.broadcastEvent( event );
		
		if(!_broadcaster.isEmpty())
		{
			_broadcaster.broadcastEvent( event );
		}
	}
	
	/**
	 * Release key down repetition locker.
	 */
	private function _stopPending() : Void
	{
		clearInterval(_pendingInterval);
		_isPending = false;
	}
	
	/**
	 * Release combo key down repetition locker.
	 */
	private function _stopPendingCombo() : Void
	{
		clearInterval(_pendingComboInterval);
		
		_keyDownList = new IndexedArray(); 
		
		_isPendingCombo = false;
	}	
}